To avoid confusions, what you call "yield" timers are actually SceneTreeTimers. You can ctrl+click on the create_timer function to open it's documentation.
How to do it
Here is an example script making use of a regular Timer named Timer, placed as a child of this script, with yields like in yours:
extends Node
var start_time: int # miliseconds
onready var timer: Timer= $Timer
func _ready() -> void:
start_time = OS.get_ticks_msec()
timer.one_shot = false # Disable repeats
timer.start(.5)
yield(timer, "timeout")
print((OS.get_ticks_msec()-start_time)*.001,
" seconds elapsed in total")
timer.start(1)
yield(timer, "timeout")
print((OS.get_ticks_msec()-start_time)*.001,
" seconds elapsed in total")
timer.start(2)
yield(timer, "timeout")
print((OS.get_ticks_msec()-start_time)*.001,
" seconds elapsed in total")
Why I dislike the question
Not against you, but against calling things "bad" and "good". You say that you want to avoid SceneTreeTimers because they are "bad", I say calling tools "bad" or "good" is a bad idea, because it hides all the important details that tell you when and how to use them. The main advantage of a SceneTreeTimer is practicality, and the main disadvantage is that an object is created every time you want a timer. Now, while this may seem problematic, you would need to create a huge number of them to notice any slowdown. Optimizing before you detect the need to is called premature optimization, and tends to make code unnecessarily complicated. Also, to make optimizations you need to check the speed of your code using tools like a profiler, otherwise you might "optimize" into code that is actually slower because you made a wrong assumption.
Now, in this case I would say that either solution works fine, I just want to warn against putting unconditional "good" or "bad" label on things.