I just came across the same topic myself and wanted to leave my solution for anyone in need in the future. Apologies if I shouldn't revive.
Note: Using Forge 1.19 as of writing
The quickest "two-liner" seems to be possible with a ScheduledExecutorService:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.schedule(() -> doSomething(), time, TimeUnit.MILLISECONDS);
Disclaimer: While I've confirmed that this works, I don't know if there exist strong conventions against using custom thread code, although this is a simple use.
My specific use case was to throw a burst of projectiles in sequence after right-clicking only once. If that's relevant, here's a more comprehensive example:
// Implementing the Item.use method to throw multiple projectiles over time
public InteractionResultHolder<ItemStack> use(Level world, Player player, InteractionHand hand)
{
ItemStack itemStack = player.getItemInHand(hand);
int itemsToThrow = Math.min(itemStack.getCount(), BURST_COUNT);
if (!world.isClientSide)
{
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
for(int i = 0; i < itemsToThrow; ++i)
{
executor.schedule(() -> throwItem(world, player, itemStack, randomsource), 200 * i, TimeUnit.MILLISECONDS);
}
}
player.awardStat(Stats.ITEM_USED.get(this), itemsToThrow);
if (!player.getAbilities().instabuild)
{
itemStack.shrink(itemsToThrow);
}
return InteractionResultHolder.sidedSuccess(itemStack, world.isClientSide());
}